package com.db.logging;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Properties;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

/**
 * @author Alexander Kotov (kotov.alex.22@gmail.com)
 */
class LogAppender implements Appender {
    private LogLevel logLevel;
    private PrintWriter out;
    private int bufferSize;
    private String logFile;
    private String logFormat;
    private final BlockingQueue<LogRecord> queue;
    private final Thread writer;
    private SimpleDateFormat dateFormat;

    private static final String INI_FILE = "/logger.properties";
    private static final String LOG_LEVEL = "logger.loglevel";
    private static final LogLevel DEFAULT_LOG_LEVEL = LogLevel.DEBUG;
    private static final String BUFFER_SIZE = "logger.buffersize";
    private static final String DEFAULT_BUFFER_SIZE = "264";
    private static final String LOG_FILE = "logger.logfile";
    private static final String DEFAULT_LOG_FILE = "log.txt";
    private static final String OUTPUT = "logger.output";
    private static final String FILE_OUTPUT = "file";
    private static final String DEFAULT_OUTPUT = "console";
    private static final String FORMAT = "logger.format";
    private static final String DEFAULT_FORMAT = "%d [%n] [%l] %m [%t]";
    private static final String DATE_FORMAT = "logger.dateformat";
    private static final String DEFAULT_DATE_FORMAT = "yyyy.MM.dd HH:mm:ss:SSS";

    private static class SingleHolder {
        public static final LogAppender LOG_APPENDER_INSTANCE = new LogAppender();
    }

    public static LogAppender getInstance() {
        return SingleHolder.LOG_APPENDER_INSTANCE;
    }

    private LogAppender() {
        Properties properties = new Properties();
        try {
            properties.load(getClass().getResourceAsStream(INI_FILE));
            try {
                logLevel = LogLevel.valueOf(properties.getProperty(LOG_LEVEL).toUpperCase());
            } catch (NullPointerException | IllegalArgumentException e) {
                System.err.format("[ERROR] : Parameter '%s' is incorrect.%n", LOG_LEVEL);
                logLevel = null;
            }
        } catch (NullPointerException | IOException exception) {
            System.err.format("[WARNING] : Failed to load '%s'. Default setting will be used.%n", INI_FILE);
        } finally {
            if (logLevel == null) {
                logLevel = DEFAULT_LOG_LEVEL;
                System.err.format("[WARNING] : Default loglevel will be used: %s.%n", logLevel);
            }
        }

        String output = properties.getProperty(OUTPUT, DEFAULT_OUTPUT);
        if (FILE_OUTPUT.equals(output)) {
            try {
                logFile = properties.getProperty(LOG_FILE, DEFAULT_LOG_FILE);
                out = new PrintWriter(new BufferedWriter(new FileWriter(logFile, true)), true);
            } catch (IOException e) {
                System.err.format("[ERROR] : Couldn't open log file '%s' for output.%n", logFile);
                out = null;
            }
        } else if (DEFAULT_OUTPUT.equals(output)) {
            out = new PrintWriter(System.out, true);
        }

        logFile = properties.getProperty(LOG_FILE, DEFAULT_LOG_FILE);
        bufferSize = Integer.parseInt(properties.getProperty(BUFFER_SIZE, DEFAULT_BUFFER_SIZE));
        logFormat = properties.getProperty(FORMAT);
        String dateFormatStr = properties.getProperty(DATE_FORMAT);

        try {
            dateFormat = new SimpleDateFormat(dateFormatStr);
        } catch (NullPointerException | IllegalArgumentException e) {
            System.err.format("[ERROR] : Parameter '%s' is incorrect.%n", DATE_FORMAT);
            dateFormat = null;
        }

        setDefaultProperties();

        queue = new ArrayBlockingQueue<>(bufferSize);
        writer = new Thread(new Writer());
        writer.setDaemon(true);
        writer.setPriority(Thread.MIN_PRIORITY);
        writer.start();
    }

    private void setDefaultProperties() {
        if (out == null) {
            out = new PrintWriter(System.out, true);
            System.err.format("[WARNING] : Output to console will be used.%n");
        }
        if (logFormat == null) {
            logFormat = DEFAULT_FORMAT;
            System.err.format("[WARNING] : Default format will be used: %s.%n", logFormat);
        }
        if (dateFormat == null) {
            dateFormat = new SimpleDateFormat(DEFAULT_DATE_FORMAT);
            System.err.format("[WARNING] : Default date format will be used: %s.%n", DEFAULT_DATE_FORMAT);
        }
    }

    @Override
    public void append(LogRecord logRecord) {
        if (logLevel.contains(logRecord.getLevel())) {
            if (!queue.offer(logRecord)) {
                System.err.format("[ERROR] : Log records queue is overfilled.%n");
            }
        }
    }

    @Override
    public LogLevel getLogLevel() {
        return logLevel;
    }

    private class Writer implements Runnable {
        @Override
        public void run() {
            while (true) {
                try {
                    LogRecord record = queue.take();
                    String string = logFormat;
                    if (string.contains("%d")) {
                        string = string.replaceFirst("%d", dateFormat.format(record.getDate()).toString());
                    }
                    if (string.contains("%t")) {
                        string = string.replaceFirst("%t", record.getThreadName());
                    }
                    if (string.contains("%n")) {
                        string = string.replaceFirst("%n", record.getLoggerName());
                    }
                    if (string.contains("%l")) {
                        string = string.replaceFirst("%l", record.getLevel().toString());
                    }
                    if (string.contains("%m")) {
                        string = string.replaceFirst("%m", record.getMessage());
                    }
                    out.println(string);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } catch (Exception e) {
                    System.err.format("[ERROR] : Writer thread caught %s (%s) %n",
                            e, e.getMessage() != null ? e.getMessage() : "");
                }
            }
        }
    }

}
